/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.struts;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.common.util.PartialCollection;
import com.blandware.atleap.model.core.ContentPage;
import com.blandware.atleap.model.core.Layout;
import com.blandware.atleap.model.core.Role;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.service.core.RoleManager;
import com.blandware.atleap.service.exception.BeanAlreadyExistsException;
import com.blandware.atleap.webapp.taglib.core.html.CancelTag;
import com.blandware.atleap.webapp.util.core.CacheUtil;
import com.blandware.atleap.webapp.util.core.GlobalProperties;
import com.blandware.atleap.webapp.util.core.TokenUtil;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import com.blandware.atleap.webapp.util.core.WebappUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.GenericValidator;
import org.apache.struts.Globals;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.InvalidCancelException;
import org.apache.struts.config.ActionConfig;
import org.apache.struts.config.ForwardConfig;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.tiles.TilesRequestProcessor;
import org.apache.struts.util.RequestUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * <p><strong>RequestProcessor</strong> contains the processing logic that
 * the Struts controller servlet performs as it receives each servlet request
 * from the container.</p>
 * <p>This processor subclasses the TilesRequestProcessor in order to intercept calls to forward
 * or include or direct requests from browser. When such calls are done, this processor checks if the specified URI
 * is content page uri from database. If true, the definition is retrieved from layout of the page and included. If
 * false, the original URI is included or a forward is performed.</p>
 * <p>For every request processed uri is stored into request attribute under PROCESSED_URI key.</p>
 * <p>For request based on Tiles definition processed uri is stored into request attribute under DEFINITION_NAME key.</p>
 * <p>When processing request, if current mapping contains forward named
 * 'unsatisfiable' then security token will be checked; if token passed through
 * request doesn't match token stored in session, then forward to
 * 'unsatisfiable' page will be done. Else, security token will be reset
 * (removed from session).
 * </p>
 * <p>Forward named 'unsatisfiableNoReset' is similar to 'unsatisfiable' with
 * one exception: security token is not reset when this forward exists in
 * current mapping.
 * </p>
 * <p>
 * If both above-mentioned forwards present, latter will be ignored.
 * </p>
 * <p><a href="ContentTilesRequestProcessor.java.html"><i>View Source</i></a></p>
 * <p/>
 *
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @version $Revision: 1.43 $ $Date: 2008/07/03 10:47:10 $
 */
public class ContentTilesRequestProcessor extends TilesRequestProcessor {

	/**
	 * Constant name used to store processed definition name in request
	 */
	public static final String DEFINITION_NAME = "com.blandware.atleap.webapp.struts.DEFINITION_NAME";

	/**
	 * Constant name used to store processed uri in request
	 */
	public static final String PROCESSED_URI = "com.blandware.atleap.webapp.struts.PROCESSED_URI";

	/**
	 * Constant name used to store full processed url with query string in request
	 */
	public static final String PROCESSED_URL = "com.blandware.atleap.webapp.struts.PROCESSED_URL";

	/**
	 * Constant name used to store content page ID in request
	 */
	public static final String CONTENT_PAGE_ID = "com.blandware.atleap.webapp.struts.CONTENT_PAGE_ID";

    /**
	 * Constant name used to store canonical uri in request
	 */
	public static final String CANONICAL_URI = "com.blandware.atleap.webapp.struts.CANONICAL_URI";

    /**
	 * Constant name used to store canonical uri last fragment (everything
     * after last slash) in request
	 */
	public static final String CANONICAL_URI_LAST_FRAGMENT = "com.blandware.atleap.webapp.struts.CANONICAL_URI_LAST_FRAGMENT";

    /**
	 * Constant name used to store marker if the current processed page is content page or not
	 */
	public static final String IS_NOCACHE = "com.blandware.atleap.webapp.struts.IS_NOCACHE";


	/**
	 * Commons Logging instance.
	 */
	protected transient final Log log = LogFactory.getLog(ContentTilesRequestProcessor.class);


	/**
	 * Initialize this request processor instance.
	 *
	 * @param servlet      The ActionServlet we are associated with.
	 * @param moduleConfig The ModuleConfig we are associated with.
	 * @throws javax.servlet.ServletException If an error occurs during initialization.
	 */
	public void init(ActionServlet servlet, ModuleConfig moduleConfig) throws ServletException {
		super.init(servlet, moduleConfig);
		if ( log.isDebugEnabled() ) {
			log.debug("Definitions factory class name: " + definitionsFactory.getClass().getName());
		}

        try {
            createUnknownRoles(servlet, moduleConfig);
        } catch (Exception e) {
            // catching just to allow application start if something went wrong
            // with creation of roles
            if (log.isErrorEnabled()) {
                log.error("Error when creating roles", e);
            }
        }
    }

    /**
	 * <p>Process an <code>HttpServletRequest</code> and create the
	 * corresponding <code>HttpServletResponse</code> or dispatch
	 * to another resource.</p>
	 *
	 * @param request  The servlet request we are processing
	 * @param response The servlet response we are creating
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a processing exception occurs
	 */
	public void process(HttpServletRequest request,
	                    HttpServletResponse response)
			throws IOException, ServletException {

		// Wrap multipart requests with a special wrapper
		request = processMultipart(request);

		// Identify the path component we will use to select a mapping
		String path = processPath(request, response);
		if ( path == null ) {
			return;
		}

		if ( log.isDebugEnabled() ) {
			log.debug("Processing a '" + request.getMethod() +
					"' for path '" + path + "'");
		}

		// Select a Locale for the current user if requested
		processLocale(request, response);

		// Set the content type and no-caching headers if requested
		processContent(request, response);
		processNoCache(request, response);

		// General purpose preprocessing hook
		if ( !processPreprocess(request, response) ) {
			return;
		}

		this.processCachedMessages(request, response);

		// Identify the mapping for this request
		ActionMapping mapping = processMapping(request, response, path);
		if ( mapping == null ) {
			return;
		}

		// Check for any role required to perform this action
		if ( !processRoles(request, response, mapping) ) {
			return;
		}

		// Note, that check for token is performed after form bean has been created and populated,
		// but before it has been validated.
		// It is done in order to prevent appearance of unsatisfiable page when Cancel button is pressed
		// and session has already been invalidated

		// Process any ActionForm bean related to this request
		ActionForm form = processActionForm(request, response, mapping);
		processPopulate(request, response, form, mapping);

		// Check for token in session
		if ( (request.getAttribute(Globals.CANCEL_KEY) == null) ) {
			if ( !checkToken(request, response, mapping) ) {
				return;
			}
		}

		// Validate form bean
        try {
            if ( !processValidate(request, response, form, mapping) ) {
                return;
            } else {
                resetToken(request, mapping);
            }
        } catch (InvalidCancelException e) {
            ActionForward forward = processException(request, response, e, form, mapping);
            processForwardConfig(request, response, forward);
            return;
        }

        // Process a forward or include specified by this mapping
		if ( !processForward(request, response, mapping) ) {
			return;
		}

		if ( !processInclude(request, response, mapping) ) {
			return;
		}

		// Create or acquire the Action instance to process this request
		Action action = processActionCreate(request, response, mapping);
		if ( action == null ) {
			return;
		}

		// Call the Action instance itself
		ActionForward forward =
				processActionPerform(request, response,
						action, form, mapping);

		// Process the returned ActionForward instance
		processForwardConfig(request, response, forward);

	}

	/**
	 * Check for token in session if <code>unsatisfiable</code> mapping exists in local forwards
	 *
	 * @param request  request
	 * @param response response
	 * @param mapping  action mapping
	 * @return Return <code>true</code> if we should continue processing, or <code>false</code> if we have already forwarded control back.
	 * @throws IOException      if an input/output error occurs.
	 * @throws ServletException if a servlet exception occurs.
	 */
	protected boolean checkToken(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping)
			throws IOException, ServletException {
		ActionForward forward = mapping.findForward("unsatisfiable");
        if (forward == null) {
            forward = mapping.findForward("unsatisfiableNoReset");
        }
		if ( forward != null ) {
			if ( !TokenUtil.getInstance().isTokenValid(request, false) ) {
				request.setAttribute("beginPage", forward.getPath());
				processForwardConfig(request, response, mapping.findForward("unsatisfiablePage"));
				return false;
			}
		}
		return true;
	}

    /**
     * Resets token if 'unsatisfiable' forward exists in current mapping
     *
     * @param request request
     * @param mapping action mapping
     */
    protected void resetToken(HttpServletRequest request, ActionMapping mapping) {
        ActionForward forward = mapping.findForward("unsatisfiable");
		if ( forward != null ) {
			TokenUtil.getInstance().resetToken(request);
		}
    }

	/**
	 * Processes a Content page.
	 * This method tries to process the parameter <code>uri</code> as a content page uri.
	 * It returns <code>true</code> if a uri has been processed, or <code>false</code> otherwise.
	 *
	 * @param uri             Uri that is suspected to be a content page uri
	 * @param contextRelative Is this page marked contextRelative ?
	 * @param request         The request we are processing
	 * @param response        The response we are creating
	 * @return <code>true</code> if the method has processed uri as a content page uri, <code>false</code> otherwise.
     * @throws java.io.IOException thrown if response.sendError() produces an
     * exception
	 */
	protected boolean processContentPage(String uri, boolean contextRelative, HttpServletRequest request, HttpServletResponse response)
			throws IOException {
		if ( log.isDebugEnabled() ) {
			log.debug("processContentPage("
					+ uri + ", "
					+ contextRelative + ")");
		}

		//generate module relative uri
		if ( contextRelative ) {
			String prefix = moduleConfig.getPrefix();
			log.debug("Strip module prefix '" + prefix + "' ");
			if ( !uri.startsWith(prefix) ) {
				log.error(getInternal().getMessage("processPath",
						request.getRequestURI()));
				try {
					response.sendError(HttpServletResponse.SC_BAD_REQUEST,
							getInternal().getMessage
									("processPath", request.getRequestURI()));
				} catch ( IOException ex ) {
					//do nothing
				}
				return true;
			}
			uri = uri.substring(prefix.length());
		}

        Long id = null;
		String cpDefinition = null;
		List roleNames = new ArrayList();
		long lastModified = -1;
		Long cacheMaxAge = null;

		//try to get from cache
		CacheUtil cacheUtil = CacheUtil.getInstance(request);
		CacheUtil.ContentPageData cpd = cacheUtil.getContentPageFromCache(uri);

		if ( cpd != null ) {
            id = cpd.getId();
            cpDefinition = cpd.getCpDefinition();
			roleNames = ConvertUtil.convertStringToList(cpd.getRoles(), ",", true);
			lastModified = cpd.getLastModified();
			cacheMaxAge = cpd.getCacheMaxAge();

			if ( log.isDebugEnabled() ) {
				log.debug("Retrieving content page from Cache with for uri=" + uri + " with cpDefinition=" + cpDefinition + " and roles=" + roleNames.toString());
			}

		} else {
			// getting from DB
			ContentPage contentPage = null;
			try {
				ApplicationContext applicationCtx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
				PageManager pageManager = (PageManager) applicationCtx.getBean(Constants.PAGE_MANAGER_BEAN);
				contentPage = pageManager.findContentPageByUri(uri);
			} catch ( Exception ex ) {
				if ( log.isErrorEnabled() ) {
					log.error("Cannot perfom function because layout could not be found. Cause: " + ex.getLocalizedMessage());
				}
			}

			//if page is found
			if ( contentPage != null ) {
				if ( contentPage.getActive().booleanValue() ) {
                    id = contentPage.getId();
                    Layout layout = contentPage.getLayout();
					cpDefinition = layout.getCpDefinition();
					lastModified = new Date().getTime();
					cacheMaxAge = contentPage.getCacheMaxAge();
					if ( cpDefinition != null ) {
						//getting names of roles
						List roles = contentPage.getRoles();
						for ( int i = 0; i < roles.size(); i++ ) {
							Role role = (Role) roles.get(i);
							roleNames.add(role.getName());
						}

						//put into cache
						String roleNamesString = null;
						if ( roleNames.size() > 0 ) {
							roleNamesString = ConvertUtil.convertListToString(roleNames, ",");
						}
						cpd = new CacheUtil.ContentPageData(id, cpDefinition, roleNamesString, cacheMaxAge);
						cacheUtil.putContentPageInCache(cpd, uri);

						if ( log.isDebugEnabled() ) {
							log.debug("Definition '" + cpDefinition + "' for layout of content page was found in database");
						}
					}
				} else {
					// the page is not active -- it's not ready for publication yet
					response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
					return true;
				}
			}

		}

		//if page was not found
		if ( cpDefinition == null ) {
			return false;
		}

		//client caching logic
		int maxAge;
		if ( cacheMaxAge != null ) {
			maxAge = cacheMaxAge.intValue();
		} else {
			maxAge = GlobalProperties.getInstance(servlet.getServletContext())
                    .getInteger(WebappConstants.CACHE_CONTENTPAGE_MAXAGE_PROPERTY, -1)
                    .intValue();
		}
		if ( maxAge < 0 ) {
			response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
			response.setHeader("Pragma", "no-cache");
			request.setAttribute(IS_NOCACHE, "true");
		} else {
			String cacheType = "public";
			if ( roleNames.size() != 0 ) {
				cacheType = "private";
			}

			String editMode = "false";
			if ( Boolean.TRUE.equals(request.getSession().getAttribute(WebappConstants.SITE_EDIT_MODE_ENABLED_KEY)) ) {
				editMode = "true";
				maxAge = 0;
			}

			response.setHeader("Cache-Control", cacheType + ",max-age=" + maxAge);
			response.setHeader("Pragma", "");

			//check for entity tag
			if ( request.getMethod().equals("GET") && lastModified != -1 ) {

				//ETag generation
				String locale = ((Locale) request.getSession(true).getAttribute(Globals.LOCALE_KEY)).getLanguage();
				String username = request.getRemoteUser();


				StringBuffer eTagBuffer = new StringBuffer("W/\"").append(locale).append("-").append(username).append("-").append(editMode).append("-").append(lastModified).append("\"");
				String eTag = eTagBuffer.toString();
				response.setHeader("ETag", eTag);

				boolean conditionSatisfied = false;
				String headerValue = request.getHeader("If-None-Match");
				if ( headerValue != null ) {
					if ( !headerValue.equals("*") ) {
						StringTokenizer commaTokenizer =
								new StringTokenizer(headerValue, ",");
						while ( !conditionSatisfied && commaTokenizer.hasMoreTokens() ) {
							String currentToken = commaTokenizer.nextToken();
							if ( currentToken.trim().equals(eTag) ) {
								conditionSatisfied = true;
							}
						}
					} else {
						conditionSatisfied = true;
					}
					if ( conditionSatisfied ) {
						response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
						return true;
					}
				}
			}
		}

		//check roles
		boolean granted = false;
		if ( roleNames != null && roleNames.size() > 0 ) {
			for ( int i = 0; i < roleNames.size(); i++ ) {
				String roleName = (String) roleNames.get(i);
				if ( request.isUserInRole(roleName) ) {
					if ( log.isDebugEnabled() ) {
						log.debug(" User '" + request.getRemoteUser() +
								"' has role '" + roleName + "', granting access");
					}
					granted = true;
					break;
				}
			}
		} else {
			granted = true;
			if ( log.isDebugEnabled() ) {
				log.debug("Anonymous user granting access");
			}
		}

		if ( !granted ) {
			// The current user is not authorized for this action
			if ( log.isDebugEnabled() ) {
				log.debug(" User '" + request.getRemoteUser() +
						"' does not have any required role, denying access");
			}
			response.sendError(HttpServletResponse.SC_FORBIDDEN,
					getInternal().getMessage("notAuthorized", uri));
			return true;
		}

		//store into request uri
		request.setAttribute(PROCESSED_URI, uri);
		request.setAttribute(PROCESSED_URL, getFullUrl(request));

        //saving ID into request
        request.setAttribute(CONTENT_PAGE_ID, id);

        //saving canonical uri
        saveCanonicalUri(request, uri);

        try {
			if ( processTilesDefinition(cpDefinition, request, response) ) {
				return true;
			} else {
				if ( log.isWarnEnabled() ) {
					log.warn("Cannot process Tiles definition specified in database");
				}
			}
		} catch ( Exception ex ) {
			if ( log.isErrorEnabled() ) {
				log.error("Cannot process Tiles definition " + ex.getLocalizedMessage());
			}
		}

		return false;
	}

    /**
	 * <p>Set the no-cache headers for all responses, if requested.
	 * <strong>NOTE</strong> - This header will be overridden
	 * automatically if a <code>RequestDispatcher.forward</code> call is
	 * ultimately invoked.</p>
	 *
	 * @param request  The servlet request we are processing
	 * @param response The servlet response we are creating
	 */
	protected void processNoCache(HttpServletRequest request, HttpServletResponse response) {
		//we override this method to enable caching manually afterwards in this class
	}

	/**
	 * Returns full reqested URL with query string
	 *
	 * @param request the request for which full URL is obtained
	 * @return full URL
	 */
	protected String getFullUrl(HttpServletRequest request) {
		StringBuffer requestUrl = request.getRequestURL();
		if ( requestUrl == null ) {
			return null;
		}
		String queryString = request.getQueryString();
		if ( queryString != null && queryString.length() > 0 ) {
			requestUrl = requestUrl.append('?').append(queryString);
		}
		return requestUrl.toString();
	}

	/**
	 * Overloaded method from RequestProcessor
	 * Selects the mapping used to process the selection path for this request.
	 * If no mapping can be identified, tries to process as content page
	 * if content page was not found creates an error response and returns
	 * <code>null</code>.
	 *
	 * @param request  The servlet request we are processing
	 * @param response The servlet response we are creating
	 * @param path     The portion of the request URI for selecting a mapping
	 * @throws java.io.IOException if an input/output error occurs
	 */
	protected ActionMapping processMapping(HttpServletRequest request, HttpServletResponse response, String path)
			throws IOException {
		// Is there a directly defined mapping for this path?
		ActionMapping mapping = (ActionMapping)
				moduleConfig.findActionConfig(path);
		if ( mapping != null ) {
			//no-cache actions
			response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
			response.setHeader("Pragma", "no-cache");
			request.setAttribute(IS_NOCACHE, "true");

			request.setAttribute(Globals.MAPPING_KEY, mapping);

			//store into request uri
			request.setAttribute(PROCESSED_URI, path);
			request.setAttribute(PROCESSED_URL, getFullUrl(request));

            //saving canonical uri
            saveCanonicalUri(request, path);

            return (mapping);
		}

		//Try to process content page
		if ( processContentPage(path, false, request, response) ) {
			return null;
		}

		//no-cache actions
		response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
		response.setHeader("Pragma", "no-cache");
		request.setAttribute(IS_NOCACHE, "true");

		// Locate the mapping for unknown paths (if any)
		ActionConfig configs[] = moduleConfig.findActionConfigs();
		for ( int i = 0; i < configs.length; i++ ) {
			if ( configs[i].getUnknown() ) {
				mapping = (ActionMapping) configs[i];
				request.setAttribute(Globals.MAPPING_KEY, mapping);
				return (mapping);
			}
		}

		// No mapping can be found to process this request
		String msg = getInternal().getMessage("processInvalid", path);
        if (log.isWarnEnabled()) {
            log.warn(msg);
        }
        response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);

		return null;
	}

	/**
	 * <p>Automatically selects a <code>Locale</code> for the current user, if requested.
	 * <strong>NOTE</strong> - configuring Locale selection will trigger
	 * the creation of a new <code>HttpSession</code> if necessary.</p>
	 *
	 * @param request  The HTTP servlet request we are proceeding
	 * @param response The HTTP servlet response we are creating
	 */
	protected void processLocale(HttpServletRequest request, HttpServletResponse response) {

        WebappUtil.ensureLocaleValidity(request);

        String localeIdentifier = null;
        Locale locale = RequestUtils.getUserLocale(request, null);
        if (locale != null) {
            localeIdentifier = locale.getLanguage();
        }

        if ( log.isDebugEnabled() ) {
			log.debug(" Setting user locale '" + localeIdentifier + "'");
		}

		response.setHeader("Content-Language", localeIdentifier);
	}

	/**
	 * Processes a Tile definition name. This overloaded method sets up definition name into request attribute
	 *
	 * @param definitionName  Definition name to insert.
	 * @param request         Current page request.
	 * @param response        Current page response.
	 * @return <code>true</code> if the method has processed uri as a definition name, <code>false</code> otherwise.
	 */
	protected boolean processTilesDefinition(String definitionName, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		request.setAttribute(DEFINITION_NAME, definitionName);
		boolean result = super.processTilesDefinition(definitionName, request, response);
		if ( !result ) {
			request.removeAttribute(DEFINITION_NAME);
		}
		return result;
	}

	/**
	 * Overloaded method from Struts' TilesRequestProcessor.
	 * Forwards or redirects to the specified destination by the specified
	 * mechanism.
	 * This method catches the Struts' actionForward call. It checks if the
	 * actionForward is done on a Tiles definition name. If true, processes the
	 * definition and inserts it. If false, behaves as original parent's method.
	 *
	 * @param request  The servlet request we are processing.
	 * @param response The servlet response we are creating.
	 * @param forward  The ActionForward controlling where we go next.
	 * @throws IOException      if an input/output error occurs.
	 * @throws ServletException if a servlet exception occurs.
	 */
	protected void processForwardConfig(HttpServletRequest request, HttpServletResponse response, ForwardConfig forward) throws IOException, ServletException {
		// Required by struts contract
		if ( forward == null ) {
			return;
		}

		if ( log.isDebugEnabled() ) {
			log.debug("processForwardConfig(" + forward.getPath() + ")");
		}

		// Try to process the definition.
		if ( processTilesDefinition(forward.getPath(), request, response) ) {
			if ( log.isDebugEnabled() ) {
				log.debug("  '" + forward.getPath() + "' - processed as definition");
			}
			return;
		}

		if ( log.isDebugEnabled() ) {
			log.debug("  '" + forward.getPath() + "' - processed as uri");
		}

		// forward doesn't contain a definition, process as in Struts' RequestProcessor

		// if we forward must be redirected to, add context path, otherwise do not
		int urlType = forward.getRedirect() ? WebappConstants.URL_TYPE_DOMAIN_RELATIVE : WebappConstants.URL_TYPE_CONTEXT_RELATIVE;
		String uri = WebappUtil.getActionForwardURL(forward, null, request, urlType, forward.getRedirect());

		if ( forward.getRedirect() ) {
			response.sendRedirect(response.encodeRedirectURL(uri));
		} else {
			doForward(uri, request, response);
		}


	}

    /**
     * <p>Populate the properties of the specified <code>ActionForm</code> instance from
     * the request parameters included with this request.  In addition,
     * request attribute <code>Globals.CANCEL_KEY</code> will be set if
     * the request was submitted with a button created by a regular
     * <code>CancelTag</code>; request attribute <code>WebappConstants.BACK_KEY</code>
     * will be set if the request was submitted with a button created by
     * 'back' type <code>CancelTag</code>.</p>
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @param form The ActionForm instance we are populating
     * @param mapping The ActionMapping we are using
     *
     * @exception ServletException if thrown by RequestUtils.populate()
     */
    protected void processPopulate(HttpServletRequest request,
                                   HttpServletResponse response,
                                   ActionForm form,
                                   ActionMapping mapping)
        throws ServletException {

        super.processPopulate(request, response, form, mapping);

        // Set attribute for 'back' button: whether it was pressed or not
        if (request.getParameter(CancelTag.BACK_PROPERTY) != null) {
            request.setAttribute(WebappConstants.BACK_KEY, Boolean.TRUE);
        }

    }

    /**
     * <p>If this request was not cancelled, was not accuired after pressing
     * 'back' button, and the request's
     * {@link ActionMapping} has not disabled validation, call the
     * <code>validate</code> method of the specified {@link ActionForm},
     * and forward to the input path if there were any errors.
     * Return <code>true</code> if we should continue processing,
     * or <code>false</code> if we have already forwarded control back
     * to the input form.</p>
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @param form The ActionForm instance we are populating
     * @param mapping The ActionMapping we are using
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     * @throws InvalidCancelException if a cancellation is attempted
     *         without the proper action configuration.
     */
    protected boolean processValidate(HttpServletRequest request,
                                      HttpServletResponse response,
                                      ActionForm form,
                                      ActionMapping mapping)
            throws IOException, ServletException, InvalidCancelException {
                                              // Was this request 'backed'?
        if (request.getAttribute(WebappConstants.BACK_KEY) != null) {
            if (log.isDebugEnabled()) {
                log.debug(" Got here after pressing 'back', skipping validation");
            }
            return (true);
        }

        return super.processValidate(request, response, form, mapping);
    }

    /**
     * Saves a canonical URI to request. It's taken from a request parameter;
     * if it's omitted, fallbackUri is saved.
     * Canonical URI last fragment is also computed and saved; it's computed
     * as everything in canonical URI after its last slash.
     *
     * @param request request
     * @param fallbackUri URI which to save of no parameter is specified
     */
    protected void saveCanonicalUri(HttpServletRequest request,
                                    String fallbackUri) {
        String canonicalUri;
        if (!GenericValidator.isBlankOrNull(request.getParameter(WebappConstants.CANONICAL_URI_KEY))) {
            canonicalUri = request.getParameter(WebappConstants.CANONICAL_URI_KEY);
        } else {
            canonicalUri = fallbackUri;
        }
        String lastFragment;
        if (canonicalUri == null) {
            lastFragment = null;
        } else {
            int index = canonicalUri.lastIndexOf('/');
            if (index == -1) {
                lastFragment = null;
            } else {
                lastFragment = canonicalUri.substring(index + 1);
            }
        }
        request.setAttribute(CANONICAL_URI, canonicalUri);
        request.setAttribute(CANONICAL_URI_LAST_FRAGMENT, lastFragment);
    }

    /**
     * Creates roles which are assigned to some actions but still not exist
     * in database. These roles are assigned to admins group.
     *
     * @param servlet       action servlet
     * @param moduleConfig  module config
     * @throws BeanAlreadyExistsException   thrown if could not create/update
     * something
     */
    protected void createUnknownRoles(ActionServlet servlet, ModuleConfig moduleConfig) throws BeanAlreadyExistsException {
        // looking for new unknown roles
        Set allRoleNames = new HashSet();
        ActionConfig[] actionConfigs = moduleConfig.findActionConfigs();
        for (int i = 0; i < actionConfigs.length; i++) {
            ActionConfig actionConfig = actionConfigs[i];
            String[] roleNames = actionConfig.getRoleNames();
            allRoleNames.addAll(Arrays.asList(roleNames));
        }

        ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servlet.getServletContext());
        RoleManager roleManager = (RoleManager) ctx.getBean(Constants.ROLE_MANAGER_BEAN);
        PartialCollection roles = roleManager.listRoles(null);
        List existingRoleNames = new ArrayList();
        Iterator it = roles.iterator();
        while (it.hasNext()) {
            Role role = (Role) it.next();
            existingRoleNames.add(role.getName());
        }

        allRoleNames.removeAll(existingRoleNames);

        if (!allRoleNames.isEmpty()) {
            if (log.isInfoEnabled()) {
                log.info("Unknown roles encountered, going to create them: " + allRoleNames);
            }
            roleManager.addRolesAndAssignToAdmins(allRoleNames, true);
            if (log.isInfoEnabled()) {
                log.info("Finished creation of roles");
            }
        }
    }
}
